<?php

namespace Application;

use Fluency;
use Fluency\Routing;
use Fluency\Utils;
use Fluency\Metadata\AnnotatedActionMetadata;

class Application
{
    const DEFAULT_FORMAT = "html";

    private $dir;
    private $frameworkDir;
    private $baseUrl;
    private $services;
    private $viewHelpers;

    public function __construct($dir, $frameworkDir, $baseUrl)
    {
        $this->dir = $dir;
        $this->frameworkDir = $frameworkDir;
        $this->baseUrl = $baseUrl;
        $this->initializeServices();
    }

    private function initializeServices()
    {
        $this->services = (object) array(
            "executor" => new Fluency\Executor(array($this, "loadAction")),
            "filterer" => new Fluency\Filterer(array($this, "loadFilter")),
            "injector" => Fluency\Injector::create()->withContainer(array($this, "getService")),
            "renderer" => new Fluency\Renderer(array($this, "loadView")),
            "baseUrl"  => $this->baseUrl,
        );
        $this->constructDispatcher();
        $this->constructRouter();
    }

    public function getService($name)
    {
        return isset($this->services->{$name}) ? $this->services->{$name} : null;
    }

    private function constructDispatcher()
    {
        $this->services->dispatcher = new Fluency\Dispatcher(
            $this->services->executor, $this->services->filterer,
            $this->services->injector, $this->services->renderer
        );
    }

    private function constructRouter()
    {
        $this->services->router = new Routing\RouterChain($this->services->dispatcher, array(
            new Routing\PresetRouter(array(
                "" => "index",
            )),
            new Routing\DecentralizedRouter($this->services->executor),
            // Specifies the HTTP 404 route
            Fluency\Request::forAction("error")->withParameter("id", 404)->withInternalScope(),
        ));
    }

    public function loadAction($name, &$flags = null)
    {
        $file = "{$this->dir}/Actions/{$name}.php";
        // Beware of directory traversal attacks!
        if (preg_match('/[^A-Za-z_\-\/]/', $name) || !is_file($file)) {
            $flags = is_dir($file) ? array() : array(Routing\DecentralizedRouter::SHOULD_STOP);
            throw new \InvalidArgumentException;
        }
        $action = require $file;
        // Metadata generation and argument mapping are rather costly operations. If a fast (e.g.
        // in-memory) cache is available, it is recommended to use it to cache the results.
        $rf = new \ReflectionFunction($action);
        $metadata = new AnnotatedActionMetadata(new Utils\AnnotationReader($rf->getDocComment()));
        return array($action, $this->mapArgumentsToServices($rf), $metadata);
    }

    public function loadFilter($name)
    {
        $filter = require "{$this->dir}/Filters/{$name}.php";
        // See the note on caching in the action loader, as it applies to filters as well.
        $rf = new \ReflectionFunction($filter);
        return array($filter, $this->mapArgumentsToServices($rf));
    }

    private function mapArgumentsToServices(\ReflectionFunction $rf)
    {
        $keys = array();
        foreach ($rf->getParameters() as $param) {
            $typeHint = $param->getClass();
            if ($typeHint) {
                $shortName = lcfirst($typeHint->getShortName());
                $keys[] = array($typeHint->getName(), $shortName, $param->getName());
            }
            $keys[] = $param->getName();
        }
        return $keys;
    }

    public function loadView($_name)
    {
        $_dir = $this->dir;
        $_helpers = $this->getViewHelpers();
        return function(array $_params) use ($_name, $_dir, $_helpers) {
            extract($_params + $_helpers);
            ob_start();
            require "{$_dir}/Views/{$_name}.php";
            return ob_get_clean();
        };
    }

    private function getViewHelpers()
    {
        $self = $this; // for use in closures
        if (!isset($this->viewHelpers)) {
            $this->viewHelpers = require "{$this->dir}/Helpers/all.php";
        }
        return $this->viewHelpers;
    }

    private function detachFormatFromUrl($url)
    {
        $lastDot = strrpos($url, ".");
        if ($lastDot !== false) {
            return array(substr($url, 0, $lastDot), substr($url, $lastDot));
        }
        return array($url, self::DEFAULT_FORMAT);
    }

    public function run($url)
    {
        list($url, $format) = $this->detachFormatFromUrl($url);
        $request = $this->services->router->createRequest($url)->preferringFormat($format);
        try {
            echo $this->services->dispatcher->dispatch($request)->sendHeaders()->getBody();
        } catch (\Exception $e) {
            $this->handleError($e);
        }
    }

    private function handleError(\Exception $e)
    {
        // Left to implement
        throw $e;
    }
}
